﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace iNet.Media.Imaging.Exif
{
	/// <summary>
	/// TIFF 影像寫入器。
	/// </summary>
	public class TiffWriter : IExifTagWriter, IComparer<ExifTag>
	{
		#region 欄位
		readonly BitConverter _BitConverter;
		readonly Dictionary<Ifd, byte[][]> _ImageData = new Dictionary<Ifd, byte[][]>();
		readonly long _ReferenceStreamPosition;
		readonly Stream _Stream;
		readonly HashSet<ExifTagKey> _TagKeys = new HashSet<ExifTagKey>();
		readonly List<ExifTag> _Tags = new List<ExifTag>();
		#endregion
		#region 建構子
		/// <summary>
		/// 初始化新的 TiffWriter 執行個體。
		/// </summary>
		/// <param name="stream">提供 TIFF 資料寫入的資料流。</param>
		/// <param name="refStreamPosition">資料流起始參考位置，資料位移的計算將參考此值。Null (Visual Basic 為 Nothing) 表示使用目前的資料流位置。</param>
		/// <param name="bitConverter">提供位元組轉換的 BitConverter。</param>
		/// <exception cref="ArgumentNullException">stream 或 bitConverter 為 Null (Visual Basic 為 Nothing)。</exception>
		/// <exception cref="ArgumentOutOfRangeException">refStreamPosition 小於零或大於 UInt32.MaxValue。</exception>
		public TiffWriter(Stream stream, long? refStreamPosition, BitConverter bitConverter)
		{
			//確認參數
			if (stream == null)
				throw new ArgumentNullException("stream");
			if (bitConverter == null)
				throw new ArgumentNullException("bitConverter");
			if (refStreamPosition != null && (refStreamPosition.Value < 0 || refStreamPosition.Value > uint.MaxValue))
				throw new ArgumentOutOfRangeException();

			//初始化欄位
			_Stream = stream;
			_ReferenceStreamPosition = (refStreamPosition == null ? _Stream.Position : refStreamPosition.Value);
			_BitConverter = bitConverter;
		}
		#endregion
		#region BitConverter
		/// <summary>
		/// 取得提供位元組轉換的 BitConverter。
		/// </summary>
		public BitConverter BitConverter
		{
			get
			{
				return _BitConverter;
			}
		}
		#endregion
		#region Commit
		/// <summary>
		/// 將 EXIF Tag 及影像資料寫入資料流。
		/// </summary>
		/// <exception cref="IOException">資料流存取錯誤。</exception>
		public void Commit()
		{
			IDictionary<ExifTagKey, long> tagPositions;
			this.Commit(out tagPositions);
		}
		/// <summary>
		/// 將 EXIF Tag 及影像資料寫入資料流。
		/// </summary>
		/// <param name="tagPositions">EXIF Tag 在資料流中的位置。</param>
		/// <exception cref="IOException">資料流存取錯誤。</exception>
		public void Commit(out IDictionary<ExifTagKey, long> tagPositions)
		{
			//初始化參數
			Dictionary<ExifTagKey, long> allTagPositions = new Dictionary<ExifTagKey, long>();
			tagPositions = allTagPositions;

			//取得影像資料
			Dictionary<Ifd, int> imageDataStripCounts = new Dictionary<Ifd, int>();
			foreach (KeyValuePair<Ifd, byte[][]> pair in _ImageData)
				imageDataStripCounts[pair.Key] = pair.Value.Length;

			//寫入標頭
			byte[] buffer8;
			if (_BitConverter.IsLittleEndian)
				buffer8 = new byte[] { (byte)'I', (byte)'I', 0x2A, 0, 8, 0, 0, 0 };
			else
				buffer8 = new byte[] { (byte)'M', (byte)'M', 0, 0x2A, 0, 0, 0, 8 };
			_Stream.Write(buffer8, 0, 8);

			//將 EXIF Tag 分類
			ExifTagClassifier tagClassifier = new ExifTagClassifier(_Tags);
			Dictionary<Ifd, List<ExifTag>> ifdTags = new Dictionary<Ifd, List<ExifTag>>();
			ifdTags[Ifd.Ifd0] = new List<ExifTag>(tagClassifier.Ifd0);
			ifdTags[Ifd.Ifd1] = new List<ExifTag>(tagClassifier.Ifd1);
			ifdTags[Ifd.Ifd2] = new List<ExifTag>(tagClassifier.Ifd2);
			ifdTags[Ifd.Ifd3] = new List<ExifTag>(tagClassifier.Ifd3);
			ifdTags[Ifd.SubIfd0] = new List<ExifTag>(tagClassifier.SubIfd0);
			ifdTags[Ifd.SubIfd1] = new List<ExifTag>(tagClassifier.SubIfd1);
			ifdTags[Ifd.ExifIfd] = new List<ExifTag>(tagClassifier.ExifIfd);
			ifdTags[Ifd.GpsIfd] = new List<ExifTag>(tagClassifier.GpsIfd);
			ifdTags[Ifd.InteroperabilityIfd] = new List<ExifTag>(tagClassifier.InteroperabilityIfd);
			List<ExifTag> makerNote = new List<ExifTag>(tagClassifier.MakerNote);
			bool hasIfd0 = (ifdTags[Ifd.Ifd0].Count > 0);

			//移除不必要的 EXIF Tag
			ifdTags[Ifd.Ifd0].RemoveAll((tag) =>
			{
				switch (tag.ID)
				{
					case 34665:		//EXIF IFD Pointer
					case 34853:		//GPS IFD Pointer
					case 273:		//StripOffsets
					case 279:		//StripByteCounts
						return true;
					default:
						return false;
				}
			});
			Predicate<ExifTag> predicate = (tag) =>
			{
				switch (tag.ID)
				{
					case 273:		//StripOffsets
					case 279:		//StripByteCounts
						return true;
					default:
						return false;
				}
			};
			ifdTags[Ifd.Ifd1].RemoveAll(predicate);
			ifdTags[Ifd.Ifd2].RemoveAll(predicate);
			ifdTags[Ifd.Ifd3].RemoveAll(predicate);
			ifdTags[Ifd.ExifIfd].RemoveAll((tag) =>
			{
				switch (tag.ID)
				{
					case 40965:		//Interoperability IFD Pointer
					case 0x927c:	//MakerNote
						return true;
					default:
						return false;
				}
			});

			//保留必要的 Tag
			if (hasIfd0)
			{
				if (makerNote.Count > 0)		//MakerNote
				{
					List<ExifTag> tags = ifdTags[Ifd.ExifIfd];
					ExifTag tag = new ExifTag(_BitConverter, Ifd.ExifIfd, 0x927c, (ExifTagType)7, 1, new byte[4]);
					int index = tags.BinarySearch(tag, this);
					tags.Insert(~index, tag);
				}
				if (ifdTags[Ifd.InteroperabilityIfd].Count > 0)	//Interoperability IFD Pointer
				{
					List<ExifTag> tags = ifdTags[Ifd.ExifIfd];
					ExifTag tag = new ExifTag(_BitConverter, Ifd.ExifIfd, 40965, ExifTagType.UInt32, 1, new byte[4]);
					int index = tags.BinarySearch(tag, this);
					tags.Insert(~index, tag);
				}
				if (ifdTags[Ifd.ExifIfd].Count > 0)		//EXIF IFD Pointer
				{
					List<ExifTag> tags = ifdTags[Ifd.Ifd0];
					ExifTag tag = new ExifTag(_BitConverter, Ifd.Ifd0, 34665, ExifTagType.UInt32, 1, new byte[4]);
					int index = tags.BinarySearch(tag, this);
					tags.Insert(~index, tag);
				}
				if (ifdTags[Ifd.GpsIfd].Count > 0)		//GPS IFD Pointer
				{
					List<ExifTag> tags = ifdTags[Ifd.Ifd0];
					ExifTag tag = new ExifTag(_BitConverter, Ifd.Ifd0, 34853, ExifTagType.UInt32, 1, new byte[4]);
					int index = tags.BinarySearch(tag, this);
					tags.Insert(~index, tag);
				}
				foreach (KeyValuePair<Ifd, int> pair in imageDataStripCounts)	//StripOffsets/StripByteCounts
				{
					List<ExifTag> tags = ifdTags[pair.Key];
					ExifTag tag = new ExifTag(_BitConverter, pair.Key, 273, ExifTagType.UInt32, (uint)pair.Value, new byte[pair.Value * 4]);
					int index = tags.BinarySearch(tag, this);
					tags.Insert(~index, tag);
					tag = new ExifTag(_BitConverter, Ifd.Ifd0, 279, ExifTagType.UInt32, (uint)pair.Value, new byte[pair.Value * 4]);
					index = tags.BinarySearch(tag, this);
					tags.Insert(~index, tag);
				}
			}

			//寫入 IFD0
			long? ifd1OffsetField;
			if (hasIfd0)
				ifd1OffsetField = this.WriteIfd(ifdTags[Ifd.Ifd0], false, allTagPositions);
			else
				ifd1OffsetField = null;

			//寫入 EXIF IFD
			uint? exifIfdOffset;
			if (ifdTags[Ifd.ExifIfd].Count > 0)
			{
				exifIfdOffset = (uint)_Stream.Position;
				this.WriteIfd(ifdTags[Ifd.ExifIfd], false, allTagPositions);
			}
			else
				exifIfdOffset = null;

			//寫入 MakerNote
			uint? makerNoteOffset;
			if (makerNote.Count > 0)
			{
				if (hasIfd0)
				{
					makerNoteOffset = (uint)_Stream.Position;
					this.WriteMakerNote(makerNote, allTagPositions);
				}
				else
				{
					makerNoteOffset = (uint)_Stream.Position;
					this.WriteIfd(makerNote, false, allTagPositions);
				}
			}
			else
				makerNoteOffset = null;

			//寫入 GPS IFD
			uint? gpsIfdOffset;
			if (ifdTags[Ifd.GpsIfd].Count > 0)
			{
				gpsIfdOffset = (uint)_Stream.Position;
				this.WriteIfd(ifdTags[Ifd.GpsIfd], false, allTagPositions);
			}
			else
				gpsIfdOffset = null;

			//寫入 IFD1
			uint? ifd1Offset;
			long ifd2OffsetField = 0;
			if (ifdTags[Ifd.Ifd1].Count > 0)
			{
				ifd1Offset = (uint)_Stream.Position;
				ifd2OffsetField = this.WriteIfd(ifdTags[Ifd.Ifd1], false, allTagPositions);
			}
			else
				ifd1Offset = null;

			//寫入 IFD2
			uint? ifd2Offset;
			long ifd3OffsetField = 0;
			if (ifdTags[Ifd.Ifd2].Count > 0)
			{
				ifd2Offset = (uint)_Stream.Position;
				ifd3OffsetField = this.WriteIfd(ifdTags[Ifd.Ifd2], false, allTagPositions);
			}
			else
				ifd2Offset = null;

			//寫入 IFD3
			uint? ifd3Offset;
			if (ifdTags[Ifd.Ifd3].Count > 0)
			{
				ifd3Offset = (uint)_Stream.Position;
				this.WriteIfd(ifdTags[Ifd.Ifd3], false, allTagPositions);
			}
			else
				ifd3Offset = null;

			//寫入 Interoperability IFD
			uint? interopIfdOffset;
			if (ifdTags[Ifd.InteroperabilityIfd].Count > 0)
			{
				interopIfdOffset = (uint)_Stream.Position;
				this.WriteIfd(ifdTags[Ifd.InteroperabilityIfd], false, allTagPositions);
			}
			else
				interopIfdOffset = null;

			//寫入影像資料
			foreach (KeyValuePair<Ifd, byte[][]> pair in _ImageData)
				this.WriteImageData(pair.Value, pair.Key, allTagPositions);
			long lastStreamPosition = _Stream.Position;

			//寫入 EXIF IFD 指標
			long tagPosition;
			if (exifIfdOffset != null && allTagPositions.TryGetValue(new ExifTagKey(Ifd.Ifd0, 34665), out tagPosition))
			{
				_BitConverter.GetBytes(exifIfdOffset.Value, buffer8, 0);
				_Stream.Position = (tagPosition + 8);
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 GPS IFD 指標
			if (gpsIfdOffset != null && allTagPositions.TryGetValue(new ExifTagKey(Ifd.Ifd0, 34853), out tagPosition))
			{
				_BitConverter.GetBytes(gpsIfdOffset.Value, buffer8, 0);
				_Stream.Position = (tagPosition + 8);
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 IFD1 指標
			if (ifd1Offset != null && ifd1OffsetField != null)
			{
				_BitConverter.GetBytes(ifd1Offset.Value, buffer8, 0);
				_Stream.Position = ifd1OffsetField.Value;
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 MakerNote 指標
			if (makerNoteOffset != null && allTagPositions.TryGetValue(new ExifTagKey(Ifd.ExifIfd, 37500), out tagPosition))
			{
				_BitConverter.GetBytes(makerNoteOffset.Value, buffer8, 0);
				_Stream.Position = (tagPosition + 8);
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 Interopability IFD 指標
			if (interopIfdOffset != null && allTagPositions.TryGetValue(new ExifTagKey(Ifd.ExifIfd, 40965), out tagPosition))
			{
				_BitConverter.GetBytes(interopIfdOffset.Value, buffer8, 0);
				_Stream.Position = (tagPosition + 8);
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 IFD2 指標
			if (ifd2Offset != null)
			{
				_BitConverter.GetBytes(ifd2Offset.Value, buffer8, 0);
				_Stream.Position = ifd2OffsetField;
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 IFD3 指標
			if (ifd3Offset != null)
			{
				_BitConverter.GetBytes(ifd3Offset.Value, buffer8, 0);
				_Stream.Position = ifd3OffsetField;
				_Stream.Write(buffer8, 0, 4);
			}

			//恢復資料流位置
			_Stream.Position = lastStreamPosition;

			//重設狀態
			_ImageData.Clear();
			_Tags.Clear();
			_TagKeys.Clear();
		}
		#endregion
		#region IComparer<ExifTag>.Compare
		int IComparer<ExifTag>.Compare(ExifTag x, ExifTag y)
		{
			if (x != null)
			{
				if (y != null)
					return ((int)x.ID - y.ID);
				return 1;
			}
			if (y != null)
				return -1;
			return 0;
		}
		#endregion
		#region ReferenceStreamPosition
		/// <summary>
		/// 取得資料流起始參考位置，資料位移的計算將參考此值。
		/// </summary>
		public long ReferenceStreamPosition
		{
			get
			{
				return _ReferenceStreamPosition;
			}
		}
		#endregion
		#region Stream
		/// <summary>
		/// 取得提供 IFD 資料寫入的資料流。
		/// </summary>
		public Stream Stream
		{
			get
			{
				return _Stream;
			}
		}
		#endregion
		#region WriteIfd
		long WriteIfd(IList<ExifTag> tags, bool includeTiffHeader, IDictionary<ExifTagKey, long> allTagPositions)
		{
			long nextOffsetField;
			IDictionary<ExifTagKey, long> tagPositions;
			IExifTagWriter tagWriter;
			if (includeTiffHeader)
				tagWriter = new TiffWriter(_Stream, null, _BitConverter);
			else
				tagWriter = new IfdWriter(_Stream, _ReferenceStreamPosition, _BitConverter);
			for (int i = 0; i < tags.Count; ++i)
				tagWriter.WriteTag(tags[i]);
			if (tagWriter is IfdWriter)
				(tagWriter as IfdWriter).Commit(out tagPositions, out nextOffsetField);
			else
			{
				tagWriter.Commit(out tagPositions);
				nextOffsetField = 0;
			}
			foreach (KeyValuePair<ExifTagKey, long> pair in tagPositions)
				allTagPositions[pair.Key] = pair.Value;
			return nextOffsetField;
		}
		#endregion
		#region WriteImageData
		void WriteImageData(byte[][] imageData, Ifd ifd, IDictionary<ExifTagKey, long> tagPositions)
		{
			//搜尋 EXIF Tag
			long stripOffsetsTagPosition, stripLengthsTagPosition;
			if (!tagPositions.TryGetValue(new ExifTagKey(ifd, 273), out stripOffsetsTagPosition)
				|| !tagPositions.TryGetValue(new ExifTagKey(ifd, 279), out stripLengthsTagPosition))
			{
				throw new ArgumentException();
			}

			//寫入影像資料
			uint[] stripOffsets, stripLengths;
			int stripCount = this.WriteImageData(imageData, out stripOffsets, out stripLengths);
			long lastStreamPosition = _Stream.Position;

			//寫入 Strip Offset
			byte[] buffer8 = new byte[8];
			_Stream.Position = (stripOffsetsTagPosition + 4);
			if (_Stream.Read(buffer8, 0, 8) < 8)
				throw new EndOfStreamException();
			if (_BitConverter.ToUInt32(buffer8) != stripCount)
				throw new ArgumentException();
			if (stripCount > 1)
			{
				_Stream.Position = (_ReferenceStreamPosition + _BitConverter.ToUInt32(buffer8, 4));
				for (int i = 0; i < stripCount; ++i)
				{
					_BitConverter.GetBytes(stripOffsets[i], buffer8, 0);
					_Stream.Write(buffer8, 0, 4);
				}
			}
			else
			{
				_Stream.Seek(-4, SeekOrigin.Current);
				_BitConverter.GetBytes(stripOffsets[0], buffer8, 0);
				_Stream.Write(buffer8, 0, 4);
			}

			//寫入 Strip Length (Strip Byte Count)
			_Stream.Position = (stripLengthsTagPosition + 4);
			if (_Stream.Read(buffer8, 0, 8) < 8)
				throw new EndOfStreamException();
			if (_BitConverter.ToUInt32(buffer8) != stripCount)
				throw new ArgumentException();
			if (stripCount > 1)
			{
				_Stream.Position = (_ReferenceStreamPosition + _BitConverter.ToUInt32(buffer8, 4));
				for (int i = 0; i < stripCount; ++i)
				{
					_BitConverter.GetBytes(stripLengths[i], buffer8, 0);
					_Stream.Write(buffer8, 0, 4);
				}
			}
			else
			{
				_Stream.Seek(-4, SeekOrigin.Current);
				_BitConverter.GetBytes(stripLengths[0], buffer8, 0);
				_Stream.Write(buffer8, 0, 4);
			}

			//恢復資料流位置
			_Stream.Position = lastStreamPosition;
		}
		int WriteImageData(byte[][] imageData, out uint[] stripOffsets, out uint[] stripLengths)
		{
			//建立陣列
			int stripCount = (imageData == null ? 0 : imageData.Length);
			stripOffsets = new uint[stripCount];
			stripLengths = new uint[stripCount];

			//寫入影像資料
			Stream stream = _Stream;
			if (stripCount > 0)
			{
				for (int i = 0; i < stripCount; i++)
				{
					byte[] strip = imageData[i];
					stripOffsets[i] = (uint)(stream.Position - _ReferenceStreamPosition);
					if (strip != null && strip.Length > 0)
					{
						stripLengths[i] = (uint)strip.Length;
						stream.Write(strip, 0, strip.Length);
					}
					else
						stripLengths[i] = 0;
				}
			}

			//回傳
			return stripCount;
		}
		/// <summary>
		/// 準備寫入影像資料，但不會將實際資料寫入至資料流。
		/// </summary>
		/// <param name="ifd">描述影像資料的 IFD。</param>
		/// <param name="imageData">準備寫入的影像資料。</param>
		/// <exception cref="ArgumentException">imageData 為空陣列 -或- imageData 包含空陣列 -或- IFD 參數值無效。</exception>
		/// <exception cref="ArgumentNullException">imageData 為 Null (Visual Basic 為 Nothing)。</exception>
		/// <exception cref="InvalidOperationException">影像資料先前已經被寫入。</exception>
		public void WriteImageData(Ifd ifd, byte[][] imageData)
		{
			if (imageData == null)
				throw new ArgumentNullException("imageData");
			if (_ImageData.ContainsKey(ifd))
				throw new InvalidOperationException();
			if (imageData.Length == 0)
				throw new ArgumentException();
			switch (ifd)
			{
				case Ifd.Ifd0:
				case Ifd.Ifd1:
				case Ifd.Ifd2:
				case Ifd.Ifd3:
					break;
				default:
					throw new ArgumentException();
			}
			for (int i = imageData.Length - 1; i >= 0; --i)
			{
				if (imageData[i] == null)
					throw new ArgumentException();
			}
			_ImageData[ifd] = imageData;
		}
		#endregion
		#region WriteMakerNote
		void WriteMakerNote(IList<ExifTag> tags, IDictionary<ExifTagKey, long> allTagPositions)
		{
			bool includeTiffHeader;
			switch (tags[0].Ifd)
			{
				case Ifd.NikonMakerNoteType1:
					{
						byte[] header = new byte[] { (byte)'N', (byte)'i', (byte)'k', (byte)'o', (byte)'n', (byte)'\0', 0x10, 0x00 };
						_Stream.Write(header, 0, header.Length);
						includeTiffHeader = true;
					}
					break;
				case Ifd.NikonMakerNoteType3:
					{
						byte[] header = new byte[] { (byte)'N', (byte)'i', (byte)'k', (byte)'o', (byte)'n', (byte)'\0', 0x02, 0x10, 0x00, 0x00 };
						_Stream.Write(header, 0, header.Length);
						includeTiffHeader = true;
					}
					break;
				default:
					includeTiffHeader = false;
					break;
			}
			this.WriteIfd(tags, includeTiffHeader, allTagPositions);
		}
		#endregion
		#region WriteTag
		/// <summary>
		/// 準備寫入 EXIF Tag，但不會將實際資料寫入至資料流。
		/// </summary>
		/// <param name="tag">準備寫入的 EXIF Tag。</param>
		/// <exception cref="ArgumentException">相同的 EXIF Tag 已經被寫入。</exception>
		/// <exception cref="ArgumentNullException">tag 為 Null (Visual Basic 為 Nothing)。</exception>
		public void WriteTag(ExifTag tag)
		{
			if (tag == null)
				throw new ArgumentNullException("tag");
			if (!_TagKeys.Add(tag.Key))
				throw new ArgumentException();
			_Tags.Add(tag);
		}
		#endregion
	}
}
